package Bilibili;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import util.PythonUtil;

import java.io.*;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static json.ParseJsonUtil.readFileContent;

/**
 * windows客户端缓存的所有文件都在根目录，没有按合集分类管理，只能通过修改时间区分哪个是刚刚缓存的
 *
 * 如何将b站缓存的视频m4s文件转换成MP4或MP3，并合成一个完整的视频
 * https://zhuanlan.zhihu.com/p/606556747
 *
 * 30080结尾的m4s文件就是视频，30280结尾的是音频
 * 要删掉文件前面的9个0，大概这是b站的一个小小的加密手法
 * 要用notepad++编辑，记事本编辑保存会破坏文件内容
 * 如果用sublimeText打开，里面30代表的ASCII的0，删掉前9个30
 * ffmpeg -i 1202214390-1-30080.m4s -i 1202214390-1-30280.m4s -codec copy output.mp4
 *
 * Edge的B站插件
 */
public class BilibiliUtilVideoWindows {

    // 视频下载位置
    private static final String videoDownloadPath = "D:\\bilibilidownload";
    // ffmpeg exe路径，如果配置了环境变量，只写名称即可，如果没有配置，就写全路径
    private static final String ffmpegExePath = "ffmpeg";
    // bv号是由数字、大写字母和小写字母通过算法来获得的一个编码，共10位
    // https://www.bilibili.com/video/BV1qi421d7z5
    // 空的话，就转换所有
    private static final String bvNo = "BV1qi421d7z5";

    public static void main(String[] args) throws Exception {
        // 根目录下所有文件夹，未按视频归类
        String filepath = videoDownloadPath;
        File file = new File(filepath);
        if (!file.exists() || !file.isDirectory()) {
            System.out.println("请指定正确的目录");
            return;
        }

        // 根目录下获取所有视频 每一集一个文件夹 没有按合集汇总
        File[] dirFiles = file.listFiles((dir, name) -> dir.isDirectory());
        if (dirFiles == null || dirFiles.length == 0) {
            System.out.println("不存在文件夹");
            return;
        }

        List<JSONObject> videoList = new ArrayList<>();
        for (File dirFile : dirFiles) {
            // 文件夹名是一串数字
            if (!dirFile.isDirectory() || !StringUtils.isNumeric(dirFile.getName())) {
                continue;
            }
            // .playurl .videoInfo 1202214390-1-30080.m4s 1202214390-1-30280.m4s
            File[] pFiles = dirFile.listFiles();
            // 视频文件 m4s
            // -30080.m4s -100101.m4s 视频 有变化
            // -30280.m4s 音频
            List<File> videoFileList = new ArrayList<>();
            // 视频信息文件 .playurl
            File playFile = null;
            // 视频信息文件 .videoInfo 分集|分P
            File partNameFile = null;
            File videoFile = null; // 视频文件
            String videoUrl = ""; // 视频文件
            File audioFile = null; // 音频文件
            String audioUrl = ""; // 视频文件

            // 读取.playurl文件，获取视频、音频文件名称 json格式
            // 不同的清晰度 视频文件的后缀名不一样 音频名称一样
            /*
            若依框架系列视频教程（RuoYi前端不分离版本）
            https://www.bilibili.com/video/BV1qi421d7z5
            01 360流畅 1466698109-1-30016 1466698109-1-30280.m4s
            02 480清晰 1466698314-1-30032.m4s 1466698314-1-30280.m4s
            03 720高清 1466698197-1-30064.m4s 1466698197-1-30280.m4s
            04 1080高清 1466698371-1-30080.m4s 1466698371-1-30280.m4s
            */
            for (File pFile : pFiles) {
                if (!pFile.isFile()) {
                    continue;
                }
                if (pFile.getName().endsWith(".playurl")) {
                    playFile = pFile;

                    String jsonString = readFileContent(playFile);
                    JSONObject jsonObject = JSON.parseObject(jsonString);

                    JSONObject dashJsonObject = jsonObject.getJSONObject("data").getJSONObject("dash");
                    JSONArray jsonArray = dashJsonObject.getJSONArray("video");
                    Object o = jsonArray.get(0);
                    if (o != null && o instanceof JSONObject) {
                        videoUrl = ((JSONObject) o).getString("baseUrl");
                    }

                    jsonArray = dashJsonObject.getJSONArray("audio");
                    o = jsonArray.get(0);
                    if (o != null && o instanceof JSONObject) {
                        audioUrl = ((JSONObject) o).getString("baseUrl");
                    }
                    break;
                }
            }

            if (StringUtils.isBlank(videoUrl) || StringUtils.isBlank(audioUrl)) {
                System.out.println("未找到视频或音频文件，" + dirFile.getAbsolutePath());
                continue;
            }

            System.out.println("name: " + dirFile.getName());
            System.out.println("videoUrl: " + videoUrl);
            System.out.println("audioUrl: " + audioUrl);
            System.out.println("-------------------");

            // 找到.videoInfo、视频、音频文件
            for (File pFile : pFiles) {
                if (!pFile.isFile()) {
                    continue;
                }
                if (pFile.getName().endsWith(".videoInfo")) {
                    partNameFile = pFile;
                }
                // if (pFile.getName().endsWith("-30080.m4s") || pFile.getName().endsWith("-30280.m4s")) {
                if (videoUrl.contains(pFile.getName()) || audioUrl.contains(pFile.getName())) {
                    videoFileList.add(pFile);
                }
                // if (pFile.getName().endsWith("-30080.m4s")) {
                if (videoUrl.contains(pFile.getName())) {
                    videoFile = pFile;
                }
                // if (pFile.getName().endsWith("-30280.m4s")) {
                if (audioUrl.contains(pFile.getName())) {
                    audioFile = pFile;
                }
            }

            if (videoFileList.size() != 2) {
                System.out.println("视频超过能处理的数量，" + dirFile.getAbsolutePath());
                continue;
            }
            if (partNameFile != null && videoFile != null && audioFile != null) {
                String jsonString = readFileContent(partNameFile);
                JSONObject jsonObject = JSON.parseObject(jsonString);

                String groupId = jsonObject.getString("groupId");
                String title = jsonObject.getString("title");
                String groupTitle = jsonObject.getString("groupTitle");
                // completed 下载完成
                String status = jsonObject.getString("status");

                if ("completed".equals(status)) {
                    jsonObject.put("videoFile", videoFile);
                    jsonObject.put("audioFile", audioFile);
                    videoList.add(jsonObject);
                }

            }
        }

        if (videoList.size() == 0) {
            System.out.println("没有文件");
            return;
        }

        // 手动按视频分组
        Map<String, List<JSONObject>> videoListMap = videoList.stream()
                .collect(Collectors.groupingBy(o -> o.getString("groupId")));

        for (Map.Entry<String, List<JSONObject>> entry : videoListMap.entrySet()) {
            String key = entry.getKey();
            List<JSONObject> values = entry.getValue();

            if (StringUtils.isNotBlank(bvNo) && !bvNo.equals(key)) {
                continue;
            }

            // 复制到一个新的目录下
            String pTitle = values.get(0).getString("groupTitle");
            File targetFile = new File(file, pTitle);
            // 已存在同名目录处理，在文件夹名称后面+副本
            while (true) {
                if (!targetFile.exists() || !targetFile.isDirectory()) {
                    // 创建目录
                    // targetFile.mkdir(); // 只能创建一层目录
                    targetFile.mkdirs(); // 创建多层目录，包括创建必需但不存在的父目录
                    break;
                }
                pTitle += "-副本";
                targetFile = new File(file, pTitle);
            }

            for (JSONObject jsonObject : values) {
                String title = jsonObject.getString("title");
                File videoFile = jsonObject.getObject("videoFile", File.class);
                File audioFile = jsonObject.getObject("audioFile", File.class);
                Integer p = jsonObject.getInteger("p");

                // 文件需要删除前面9个0
                // file 转 byte[]数组
                byte[] videoBytes1 = File2byte(videoFile);
                byte[] videoBytes2 = new byte[videoBytes1.length - 9];
                // java Byte数据截取 https://blog.csdn.net/qq_27566167/article/details/126405263
                System.arraycopy(videoBytes1, 9, videoBytes2, 0, videoBytes2.length);
                videoFile = upload(videoBytes2, targetFile.getAbsolutePath(), "video.m4s");

                byte[] audioBytes1 = File2byte(audioFile);
                byte[] audioBytes2 = new byte[audioBytes1.length - 9];
                System.arraycopy(audioBytes1, 9, audioBytes2, 0, audioBytes2.length);
                audioFile = upload(audioBytes2, targetFile.getAbsolutePath(), "audio.m4s");

                File dest = new File(targetFile, p + "." + title + ".mp4");

                // 合并音视频
                if (videoFile != null && audioFile != null) {
                    ffmpegMerge(videoFile.getAbsolutePath(), audioFile.getAbsolutePath(), dest.getAbsolutePath());
                    // break;
                }

            }
        }


    }

    /**
     * file 转 byte[]数组
     */
    public static byte[] convertFileToByteArray(File file) {
        try {
            FileInputStream fis = new FileInputStream(file);
            byte[] byteArray = new byte[(int) file.length()];
            fis.read(byteArray);
            fis.close();
            return byteArray;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /*
     * java文件转换成byte数组以及byte数组上传到文件
     * https://blog.csdn.net/m0_60215634/article/details/122755800
     */
    public static byte[] File2byte(File filePath) {
        byte[] buffer = null;
        try {
            FileInputStream fis = new FileInputStream(filePath);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer;
    }

    /**
     * @param bytes     byte数组
     * @param fileRoute 文件路径
     * @param fileName  文件名
     */
    public static File upload(byte[] bytes, String fileRoute, String fileName) {
        try {
            File directory = new File(fileRoute);
            if (!directory.exists()) {
                directory.mkdirs();
            }
            File file = new File(directory, fileName);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            bos.write(bytes);
            bos.close();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将内容写到目标文件
     */
    private static void writeFile(String newContent, String targetFile) throws Exception {
        File file = new File(targetFile);
        FileOutputStream fos = new FileOutputStream(file);
        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
        osw.write(newContent);
        osw.flush();
        osw.close();
        fos.close();
    }

    /**
     * java实现音视频的合并
     * https://blog.csdn.net/maziaotong/article/details/127863727
     * 使用 ffmpeg 来合并音频和视频文件 https://www.cnblogs.com/dragona/p/17206097.html
     *
     * @param videoFilePath 原视频路径
     * @param audioFilePath 原视频路径
     * @param desFilePath   合并后视频存放路径+视频名称
     */
    public static void ffmpegMerge(String videoFilePath, String audioFilePath, String desFilePath) {
        // ffmpeg -i 原视频路径 -i 原音频路径 -codec copy 合并后视频存放路径+视频名称

        // 解释器
        String exe = ffmpegExePath;
        // 组合成一个字符串数组
        // 执行报错：Unrecognized option 'codec copy' https://www.jianshu.com/p/ea996d79a231
        // String[] cmdArr = new String[]{exe, "-i", videoFilePath, "-i", audioFilePath, "-codec copy", desFilePath};
        // 解决报错："-codec copy" 要拆开，数组元素不带空格
        String[] cmdArr = new String[]{exe, "-loglevel", "quiet", "-i", videoFilePath, "-i", audioFilePath, "-codec", "copy", desFilePath};

        PythonUtil.executePythonScript(cmdArr);

        String cmdStr = MessageFormat.format(exe + " -i {0} -i {1} -codec copy {2}", videoFilePath, audioFilePath, desFilePath);
        System.out.println("cmdStr: " + cmdStr);
        // PythonUtil.executeCmd(cmdStr);

    }

    /**
     * 使用ffmpeg批量合并flv文件
     * ffmpeg -f concat -i mylist.txt -c copy output.flv
     * https://www.likecs.com/show-379618.html
     * ffmpeg将多个flv文件合成为mp4（python版） https://www.jianshu.com/p/bfda71bad87d
     * 通过ffmpeg将FLV文件转换为MP4 https://blog.csdn.net/CN_Sue/article/details/122290494
     *
     * @param desFilePath 合并后视频存放路径+视频名称
     */
    public static void ffmpegMergeFlv(String listFilePath, String desFilePath) {
        // 拼接完，只有第一个可以播放
        // ffmpeg -i "concat:17222411_2_0.flv|17222411_2_1.flv|17222411_2_2.flv" -codec copy out_mp4.mp4
        // 3个都可以播放
        // ffmpeg -f concat -i list.txt -codec copy out_mp42.mp4

        // PythonUtil.executePythonScript(cmdArr);

        // 解决报错：-safe 0
        // https://www.jianshu.com/p/ef54a7cdcb7f
        // https://www.cnblogs.com/han-1034683568/p/11515459.html
        // [concat @ 000001bbe578d3c0] Unsafe file name 'D:bilibiliload17222411117222411_1_0.flv'
        // [in#0 @ 000001bbe5777500] Error opening input: Operation not permitted

        // 解决报错：文件路径加单引号
        // Impossible to open 'D:bilibiliload17222411117222411_1_0.flv'

        // -loglevel quiet 如果报错可以删掉，放出日志
        // ffmpeg设置终端不显示日志 https://www.cnblogs.com/chentiao/p/17174382.html
        String cmdStr = MessageFormat.format("ffmpeg -loglevel quiet -f concat -safe 0 -i {0} -codec copy {1}", listFilePath, desFilePath);
        System.out.println("cmdStr: " + cmdStr);
        PythonUtil.executeCmd(cmdStr);

    }

}
